﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MangaGrr.DomainModel;
using MangaGrr.Logic;
using MangaGrr.MgrEventArgs;
using MinhUtils.Threading;
using System.Diagnostics;
using System.Threading;
using MinhUtils;

namespace MangaGrr
{
    class MgrDownloader
    {
        private int _totalFetchThread = 8;
        private const int TOTAL_CONNECTION = 16;

        /// <summary>
        /// A callback method which will be called everytime the progress is changed. If not necessary, set to be null.
        /// </summary>
        internal event Action<object, DownloadProgressChangedEventArgs> DownloadProgressChanged;
        /// <summary>
        /// A callback method which will be called when download is completed. If not necessary, set to be null.
        /// </summary>
        internal event EventHandler DownloadCompleted;

        private readonly Queue<MangaChapterDownloader> _queueManaMangaChapterDownloaders = new Queue<MangaChapterDownloader>();
        private readonly ConcurrentQueue<MangaPage> _pageQueue = new ConcurrentQueue<MangaPage>();
        //private ConcurrentQueue<Chapter> _zipQueue = new ConcurrentQueue<Chapter>(); //ZIP
        private string _saveTo;

        private readonly Stopwatch _stopWatch = new Stopwatch();

        private volatile int _fetchCompletedCount = 0;
        private volatile bool _fetchCompleted = false;
        private volatile bool _userAbort = false;
        private volatile bool _stopFetching = false;
        private volatile bool _stopDownloading = false;
        private volatile bool _stopCalculatingProgress = false;
        private volatile bool _soundEnable = true;

        private volatile int _totalChapter = 0;
        private volatile int _totalChapterFetched = 0;
        private volatile int _totalPageDownloaded = 0;
        private volatile int _totalPageFetched = 0;
        private double _currentPercentage = 0;

        private Thread _progressThread;
        private readonly List<Thread> _fetchThreads = new List<Thread>();
        private readonly List<Thread> _downloadThreads = new List<Thread>();

        private readonly Mutex _mutexFetchCompletedCount = new Mutex();
        private readonly Mutex _mutexTotalPageDownloaded = new Mutex();
        private readonly Mutex _mutexTotalChapterFetched = new Mutex();
        private readonly Mutex _mutexFetchThreads = new Mutex();
        private readonly Mutex _mutexDownloadThreads = new Mutex();
        private readonly Mutex _mutexChapterQueue = new Mutex();

        /// <summary>
        /// Download the given list of chapters to a specified folder.
        /// </summary>
        /// <param name="colMangaChapterDownloaders">List of chapters to be downloaded</param>
        /// <param name="saveTo">Where to save these chapters</param>
        internal void StartDownloading(ICollection<MangaChapterDownloader> colMangaChapterDownloaders, string saveTo)
        {
            _stopWatch.Start();
            MGRUtil.WriteLog("Download started. Total chapter: " + colMangaChapterDownloaders.Count);

            //Initialize
            _totalChapter = colMangaChapterDownloaders.Count;
            foreach (var chapter in colMangaChapterDownloaders)
            {
                chapter.TotalPageLoaded += chapter_TotalPageLoaded;
                chapter.NewPageLoaded += chapter_NewPageLoaded;
                _queueManaMangaChapterDownloaders.Enqueue(chapter);
            }

            _saveTo = saveTo;

            NetUtil.SetConnectionLimit(colMangaChapterDownloaders.First().MangaChapter.URL, Properties.Settings.Default.ConnectionLimit);

            if (_totalFetchThread > _totalChapter)
                _totalFetchThread = _totalChapter;

            //Start threads
            new Thread(Monitor).Start();

            for (int i = 0; i < _totalFetchThread; i++)
            {
                Thread fetchThread = new Thread(Fetch);
                fetchThread.Start();
                _fetchThreads.Add(fetchThread);
            }

            for (int i = 0; i < TOTAL_CONNECTION - _totalFetchThread; i++)
            {
                Thread downloadThread = new Thread(Download);
                downloadThread.Start();

                _downloadThreads.Add(downloadThread);
            }

            _progressThread = new Thread(CalculateProgress);
            _progressThread.Start();
        }

        /// <summary>
        /// Add more chapters to currently downloading process, only after Downloader has started downloading (by calling StartDownloading function)
        /// </summary>
        /// <param name="chapterList">List of chapters to be added</param>
        internal void AddManga(ICollection<MangaChapter> chapterList)
        {
            //case1: all fetching threads have not finished fetching
            if (_totalFetchThread == _fetchThreads.Count)
            {
            }

            //case2: all fetching threads have finished fetching and stopped

            //case3: some but not all fetching threads have finished fetching and stopped, some are still fetching
        }

        private void chapter_TotalPageLoaded(object sender, TotalPageLoadedEventArgs e)
        {
            _mutexTotalChapterFetched.WaitOne();

            _totalPageFetched += e.TotalPage;
            _totalChapterFetched++;

            _mutexTotalChapterFetched.ReleaseMutex();

            if (_soundEnable)
                System.Media.SystemSounds.Asterisk.Play();

            //TODO sent chapter url + total page back to main form for displaying
            //Alternative: send percentage downloaded (of each chapter) back to main form
        }

        private void chapter_NewPageLoaded(object sender, NewPageLoadedEventArgs e)
        {
            _pageQueue.Enqueue(e.NewPage);
        }

        /// <summary>
        /// Stop current downloading process and stop all working threads
        /// </summary>
        internal void AbortDownloading()
        {
            _userAbort = true;
        }

        /// <summary>
        /// Fetch chapters' pages's url into queue, if there's no more page to scrap then start a new thread to download pages
        /// </summary>
        private void Fetch()
        {
            while (_stopFetching == false)
            {
                MangaChapterDownloader mangaChapterDownloader = null;
                try
                {
                    _mutexChapterQueue.WaitOne();
                    if (_queueManaMangaChapterDownloaders.Count > 0)
                    {
                        mangaChapterDownloader = _queueManaMangaChapterDownloaders.Dequeue();
                    }
                    _mutexChapterQueue.ReleaseMutex();

                    if (mangaChapterDownloader != null)
                    {
                        mangaChapterDownloader.LoadPage();
                    }
                    else //no more chapter
                    {
                        _mutexFetchCompletedCount.WaitOne();
                        _fetchCompletedCount++;
                        _mutexFetchCompletedCount.ReleaseMutex();

                        //yay
                        _fetchCompleted = (_fetchCompletedCount == _totalFetchThread);

                        //start a new download thread and add to list (to manage later)
                        Thread downloadThread = new Thread(new ThreadStart(Download));
                        downloadThread.Start();

                        _mutexDownloadThreads.WaitOne();
                        _downloadThreads.Add(downloadThread);
                        _mutexDownloadThreads.ReleaseMutex();

                        //get out of loop
                        break;
                    }
                }
                catch (Exception ex)
                {
                    if (mangaChapterDownloader != null)
                        MGRUtil.WriteLog(string.Format("Exception while fetching MS{0}/MG{1}/CH{2}: {3}", mangaChapterDownloader.MangaChapter.Manga.MangaSite.Name, mangaChapterDownloader.MangaChapter.Manga.Name, mangaChapterDownloader.MangaChapter.OrderInfo, ex));
                }
            }

            //remove from fetching threads list
            _mutexFetchThreads.WaitOne();
            _fetchThreads.Remove(Thread.CurrentThread);
            _mutexFetchThreads.ReleaseMutex();
        }

        /// <summary>
        /// Download pages in queue to saveTo folder
        /// </summary>
        private void Download()
        {
            while (_stopDownloading == false)
            {
                MangaPage mangaPage = null;
                try
                {
                    mangaPage = _pageQueue.TryDequeue();

                    if (mangaPage == null)
                        break;

                    new MangaPageDownloader(mangaPage).Download(_saveTo, 1000);

                    _mutexTotalPageDownloaded.WaitOne();
                    _totalPageDownloaded++;
                    _mutexTotalPageDownloaded.ReleaseMutex();

                    if (_soundEnable)
                        System.Media.SystemSounds.Exclamation.Play();

                    //if (page.Number == page.Chapter.TotalPage) //ZIP
                    //{
                    //    _zipQueue.Enqueue(page.Chapter);
                    //}
                }
                catch (Exception ex)
                {
                    MGRUtil.WriteLog(string.Format("Exception while downloading MS{0}/MG{1}/CH{2}/PG{3}: {4}", mangaPage.Chapter.Manga.MangaSite.Name, mangaPage.Chapter.Manga.Name, mangaPage.Chapter.OrderInfo, mangaPage.Number, ex.ToString()));
                }
            }

            //remove from downloading threads list
            _mutexDownloadThreads.WaitOne();
            _downloadThreads.Remove(Thread.CurrentThread);
            _mutexDownloadThreads.ReleaseMutex();
        }

        /// <summary>
        /// Monitor fetching and downloading threads, when download completed, terminate all waiting threads and call CompleteDownloadingDelegate
        /// </summary>
        private void Monitor()
        {
            while (true)
            {
                if (_userAbort == true)
                {
                    _stopFetching = true;
                    _stopDownloading = true;
                    break;
                }
                else if (_pageQueue.Size == 0 && _fetchCompleted == true)
                {
                    //_stopFetching = true;
                    _stopDownloading = true;
                    break;
                }
                else
                {
                    Thread.Sleep(1000);
                }
            }

            Sayonara();
        }

        /// <summary>
        /// Clean up and leave
        /// </summary>
        private void Sayonara()
        {
            //clean up threads
            while (_downloadThreads.Count != 0)
            {
                for (int i = 0; i < _downloadThreads.Count && _pageQueue.Size < _downloadThreads.Count; i++)
                {
                    _pageQueue.Enqueue(null);
                }

                Thread.Sleep(100);
            }

            _stopCalculatingProgress = true;
            _stopWatch.Stop();

            Report();

            if (DownloadCompleted != null)
                DownloadCompleted.Invoke(this, new EventArgs());
        }

        private void Report()
        {
            //report
            if (_userAbort == false)
            {
                StringBuilder report = new StringBuilder("Download completed. ");
                report.Append("Total time: " + StringUtil.ToHHMMSS(_stopWatch.Elapsed.TotalSeconds) + ". ");
                double avg = Math.Round(_stopWatch.Elapsed.TotalSeconds / _totalChapter, 3);
                report.Append("Total chapters: " + _totalChapter + "(Avg: " + avg + " seconds/chapter). ");
                avg = Math.Round(_stopWatch.Elapsed.TotalSeconds / _totalPageDownloaded, 3);
                report.Append("Total pages: " + _totalPageDownloaded + "(Avg: " + avg + " seconds/page). ");

                MGRUtil.WriteLog(report.ToString());
            }
            else //aborted by user
            {
                StringBuilder report = new StringBuilder("Download aborted. ");
                report.Append("Total time: " + StringUtil.ToHHMMSS(_stopWatch.Elapsed.TotalSeconds) + ". ");
                double avg = Math.Round(_stopWatch.Elapsed.TotalSeconds / _totalPageDownloaded, 3);
                report.Append("Total pages: " + _totalPageDownloaded + "(Avg: " + avg + " seconds/page). ");

                MGRUtil.WriteLog(report.ToString());
            }
        }

        private void CalculateProgress()
        {
            while (_stopCalculatingProgress == false)
            {
                double estimatedTotalPage = ((double)_totalPageFetched / _totalChapterFetched * _totalChapter);
                double percentage = Common.GetPercentageDouble(_totalPageDownloaded, estimatedTotalPage);
                double timeEclapsed = _stopWatch.Elapsed.TotalSeconds;
                double estimatedTotalTime = (timeEclapsed / _totalPageDownloaded * estimatedTotalPage);
                double estimatedTimeLeft = estimatedTotalTime - timeEclapsed;

                if (percentage > _currentPercentage)
                {
                    _currentPercentage = percentage;
                }

                if (DownloadProgressChanged != null)
                {
                    DownloadProgressChangedEventArgs e = new DownloadProgressChangedEventArgs();
                    e.PercentCompleted = _currentPercentage;
                    e.TimeEclapsed = timeEclapsed;
                    e.TimeLeft = estimatedTimeLeft;
                    e.TotalFetchingThreads = _fetchThreads.Count;
                    e.TotalDownloadingThreads = _downloadThreads.Count;

                    DownloadProgressChanged.Invoke(this, e);
                }

                Thread.Sleep(1000); //sleep for 1 second
            }
        }

        internal void EnableSound(bool value)
        {
            _soundEnable = value;
        }
    }
}
